﻿using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using SilverlightViewport.Common;
using SilverlightViewport.Common.Interop;

namespace SilverlightViewport.WPF
{
    public enum ViewportState
    {
        Manual,
        Render,
        Closed
    }

    public class SilverlightViewportElement : FrameworkElement
    {
        #region Local Instances
        /// <summary>
        /// The control that loads Silverlight and handles rendering
        /// to a frame buffer.  This class does not do any presentation.
        /// </summary>
        private SilverlightWindowless m_silverlightWindowless;

        /// <summary>
        /// The is our WPF pixel renderer.  It presents frames that have
        /// been rendered by Silverlight.
        /// </summary>
        private readonly RasterRendererElement m_renderer;

        /// <summary>
        /// Back store for the XAML property.  This string is XAML for
        /// Silverlight to render.
        /// </summary>
        private string m_xaml;

        /// <summary>
        /// Reference to the current window we belong to
        /// </summary>
        private Window m_currentWindow;

        /// <summary>
        /// Reference to the current HwndSource of this visual
        /// </summary>
        private HwndSource m_hwndSource;

        #endregion

        public event EventHandler<SilverlightErrorEventArgs> SilverlightError;

        private void InvokeSilverlightError(SilverlightErrorEventArgs e)
        {
            EventHandler<SilverlightErrorEventArgs> error = SilverlightError;
            if (error != null) error(this, e);
        }

        #region Constructors
        public SilverlightViewportElement()
        {
            Focusable = true;

            Loaded += SilverlightViewportElement_Loaded;
            Unloaded += SilverlightViewportElement_Unloaded;
            m_renderer = new InteropBitmapRenderer();

            AddVisualChild(m_renderer);
        }
        #endregion


        #region Source

        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(string), typeof(SilverlightViewportElement),
                new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.None, OnSourceChanged));

        public string Source
        {
            get { return (string)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SilverlightViewportElement)d).OnSourceChanged(e);
        }

        protected virtual void OnSourceChanged(DependencyPropertyChangedEventArgs e)
        {
            var source = (string) e.NewValue;

            if (m_silverlightWindowless != null)
                m_silverlightWindowless.BeginInvoke((Action)(() => m_silverlightWindowless.Source = source));

        }

        #endregion

        #region BaseUrl
        public static readonly DependencyProperty BaseUrlProperty =
            DependencyProperty.Register("BaseUrl", typeof(string), typeof(SilverlightViewportElement),
                new FrameworkPropertyMetadata(string.Empty,
                    FrameworkPropertyMetadataOptions.None,OnBaseUrlChanged));

        public string BaseUrl
        {
            get { return (string)GetValue(BaseUrlProperty); }
            set { SetValue(BaseUrlProperty, value); }
        }

        private static void OnBaseUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SilverlightViewportElement)d).OnBaseUrlChanged(e);
        }

        protected virtual void OnBaseUrlChanged(DependencyPropertyChangedEventArgs e)
        {
            var baseUrl = (string) e.NewValue;

            if (m_silverlightWindowless != null)
                m_silverlightWindowless.BaseUrl = baseUrl;
        }

        #endregion

        #region RenderFps

        public static readonly DependencyProperty RenderFpsProperty =
            DependencyProperty.Register("RenderFps", typeof(int), typeof(SilverlightViewportElement),
                new FrameworkPropertyMetadata(60, new PropertyChangedCallback(OnRenderFpsChanged)));

        public int RenderFps
        {
            get { return (int)GetValue(RenderFpsProperty); }
            set { SetValue(RenderFpsProperty, value); }
        }

        private static void OnRenderFpsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SilverlightViewportElement)d).OnRenderFpsChanged(e);
        }

        protected virtual void OnRenderFpsChanged(DependencyPropertyChangedEventArgs e)
        {
            if (m_silverlightWindowless == null)
                return;

            int fps = (int) e.NewValue;

            m_silverlightWindowless.Invoke((Action) (() => m_silverlightWindowless.RenderFps = fps));
        }

        #endregion

        #region ViewportSize

        public static readonly DependencyProperty ViewportSizeProperty =
            DependencyProperty.Register("ViewportSize", typeof(Size), typeof(SilverlightViewportElement),
                new FrameworkPropertyMetadata(new Size(600, 600),
                    new PropertyChangedCallback(OnViewportSizeChanged)));

        public Size ViewportSize
        {
            get { return (Size)GetValue(ViewportSizeProperty); }
            set { SetValue(ViewportSizeProperty, value); }
        }

        private static void OnViewportSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SilverlightViewportElement)d).OnViewportSizeChanged(e);
        }

        protected virtual void OnViewportSizeChanged(DependencyPropertyChangedEventArgs e)
        {
            if (m_silverlightWindowless == null)
                return;
            
            var size = (Size) e.NewValue;

            m_silverlightWindowless.BeginInvoke((Action) delegate
            {
                m_silverlightWindowless.Size = new System.Drawing.Size((int)size.Width, (int)size.Height);
            });
        }

        #endregion

        #region LoadedBehavior

        public static readonly DependencyProperty LoadedBehaviorProperty =
            DependencyProperty.Register("LoadedBehavior", typeof(ViewportState), typeof(SilverlightViewportElement),
                new FrameworkPropertyMetadata(ViewportState.Render,
                    FrameworkPropertyMetadataOptions.None,OnLoadedBehaviorChanged));

        public ViewportState LoadedBehavior
        {
            get { return (ViewportState)GetValue(LoadedBehaviorProperty); }
            set { SetValue(LoadedBehaviorProperty, value); }
        }

        private static void OnLoadedBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SilverlightViewportElement)d).OnLoadedBehaviorChanged(e);
        }

        protected virtual void OnLoadedBehaviorChanged(DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        #region UnloadedBehavior

        public static readonly DependencyProperty UnloadedBehaviorProperty =
            DependencyProperty.Register("UnloadedBehavior", typeof(ViewportState), typeof(SilverlightViewportElement),
                new FrameworkPropertyMetadata(ViewportState.Closed,
                    new PropertyChangedCallback(OnUnloadedBehaviorChanged)));

        public ViewportState UnloadedBehavior
        {
            get { return (ViewportState)GetValue(UnloadedBehaviorProperty); }
            set { SetValue(UnloadedBehaviorProperty, value); }
        }

        private static void OnUnloadedBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SilverlightViewportElement)d).OnUnloadedBehaviorChanged(e);
        }

        protected virtual void OnUnloadedBehaviorChanged(DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        #region Message Loop Hooks
        private void HookHwndProc()
        {
            UnhookHwndProc();

            if (m_hwndSource == null)
                return;

            m_hwndSource.AddHook(WndProcHook);
        }

        private void UnhookHwndProc()
        {
            if (m_hwndSource == null)
                return;

            m_hwndSource.RemoveHook(WndProcHook);
        }

        private IntPtr WndProcHook(IntPtr hwnd, int message, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (m_silverlightWindowless == null)
                return IntPtr.Zero;

            if(IsFocused)
            {
                if (message == WindowsMessages.WM_CHAR || 
                    message == WindowsMessages.WM_KEYDOWN || 
                    message == WindowsMessages.WM_KEYUP)
                {
                    m_silverlightWindowless.SendMessage((uint)message, wParam, lParam);
                }
            }

            return IntPtr.Zero;
        }
        #endregion

        private void SilverlightViewportElement_Unloaded(object sender, RoutedEventArgs e)
        {
            if (m_currentWindow != null)
                m_currentWindow.Closed -= CurrentWindow_Closed;

            UnhookHwndProc();
            
            m_hwndSource = null;

            ExecuteBehavior(UnloadedBehavior);
        }

        private void SilverlightViewportElement_Loaded(object sender, RoutedEventArgs e)
        {
            m_hwndSource = PresentationSource.FromVisual(this) as HwndSource;

            HookHwndProc();

            if(m_currentWindow != null)
                m_currentWindow.Closed -= CurrentWindow_Closed;

            m_currentWindow = Window.GetWindow(this);

            if(m_currentWindow!= null)
                m_currentWindow.Closed += CurrentWindow_Closed;

            ExecuteBehavior(LoadedBehavior);
        }

        private void CurrentWindow_Closed(object sender, EventArgs e)
        {
            UnhookHwndProc();

            m_hwndSource = null;

            ExecuteBehavior(UnloadedBehavior);
        }

        public void Open()
        {
            ExecuteBehavior(ViewportState.Render);
        }

        public void Close()
        {
            ExecuteBehavior(ViewportState.Closed);
        }

        private void ExecuteBehavior(ViewportState state)
        {
            switch (state)
            {
                case ViewportState.Render:
                    if(m_silverlightWindowless == null)
                        InitializeSilverlight();
                    break;
                case ViewportState.Closed:
                    ReleaseResources();
                    break;
                case ViewportState.Manual:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void ReleaseResources()
        {
            if(m_silverlightWindowless != null)
            {
                m_silverlightWindowless.OnRendered -= SilverlightWindowless_OnRendered;
                m_silverlightWindowless.SilverlightError -= SilverlightWindowless_SilverlightError;

                m_silverlightWindowless.Dispose();
            }

            if(m_renderer != null)
            {
                m_renderer.Dispose();
            }
        }

        private void InitializeSilverlight()
        {
            ReleaseResources();

            var size = ViewportSize;
            var source = Source;
            var baseUrl = BaseUrl;
            var fps = RenderFps;

            m_silverlightWindowless = new SilverlightWindowless
            {
                 Width = (int) size.Width,
                 Height = (int) size.Height,
                 BaseUrl = baseUrl,
                 RenderFps = fps
            };
            
            m_silverlightWindowless.SilverlightError += SilverlightWindowless_SilverlightError;

            m_silverlightWindowless.Source = source;

            m_silverlightWindowless.Xaml = m_xaml;

            /* The on rendered event informs us when a new frame has been rendered
            * and is ready for presentation */
            m_silverlightWindowless.OnRendered += SilverlightWindowless_OnRendered;
        }

        private void SilverlightWindowless_SilverlightError(object sender, SilverlightErrorEventArgs e)
        {
            InvokeSilverlightError(e);
        }

        private void SilverlightWindowless_OnRendered(object sender, RenderedFrameEventArgs e)
        {
            /* Send the frame to our pixel renderer */
            m_renderer.RenderFrame(e);
        }

        #region Interaction
        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed && m_silverlightWindowless != null)
            {
                Keyboard.Focus(this);
                
                Point p = e.GetPosition(this);
                p.X = p.X / ActualWidth;
                p.Y = p.Y / ActualHeight;
                m_silverlightWindowless.BeginInvoke((Action)(() => m_silverlightWindowless.SendLeftButtonDown(p.X, p.Y)));
            }
            CaptureMouse();
            base.OnMouseDown(e);
        }

        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Released && m_silverlightWindowless != null)
            {
                Point p = e.GetPosition(this);
                p.X = p.X / ActualWidth;
                p.Y = p.Y / ActualHeight;
                m_silverlightWindowless.BeginInvoke((Action)(() => m_silverlightWindowless.SendLeftButtonUp(p.X, p.Y)));
            }

            ReleaseMouseCapture();
            base.OnMouseUp(e);
        }

        protected override void OnMouseWheel(MouseWheelEventArgs e)
        {
            if(m_silverlightWindowless != null)
                m_silverlightWindowless.BeginInvoke((Action)(() => m_silverlightWindowless.SendScroll(e.Delta)));
            
            base.OnMouseWheel(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if(m_silverlightWindowless != null)
            {
                var p = e.GetPosition(this);
               
                p.X = p.X / ActualWidth;
                p.Y = p.Y / ActualHeight;
                m_silverlightWindowless.BeginInvoke((Action)(() => m_silverlightWindowless.SendMouseMove(p.X, p.Y)));
            }
            
            base.OnMouseMove(e);
        }
        #endregion

        public object SilverlightContent
        {
            get
            {
                if (m_silverlightWindowless != null)
                    return m_silverlightWindowless.SilverlightContent;

                return null;
            }
        }

        public string Xaml
        {
            get
            {
                if (m_silverlightWindowless == null)
                    return m_xaml;

                return m_silverlightWindowless.Xaml;
            }
            set
            {
                m_xaml = value;
                if (m_silverlightWindowless == null)
                    InitializeSilverlight();
                else
                    m_silverlightWindowless.Xaml = m_xaml;
            }
        }

        #region General Overrides
        protected override Size MeasureOverride(Size availableSize)
        {
            m_renderer.Measure(availableSize);
            return m_renderer.DesiredSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            m_renderer.Arrange(new Rect(finalSize));
            return finalSize;
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return 1;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index > 0)
                throw new IndexOutOfRangeException();

            return m_renderer;
        }
        #endregion
    }
}
